/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007-2008 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.manager.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import org.apache.commons.betwixt.io.BeanReader;
import org.apache.commons.betwixt.io.BeanWriter;
import org.apache.commons.betwixt.strategy.ObjectStringConverter;
import org.apache.commons.betwixt.strategy.SimpleTypeMapper;
import org.apache.commons.betwixt.strategy.TypeBindingStrategy;
import org.xml.sax.InputSource;

import com.genia.toolbox.basics.bean.Marshaller;
import com.genia.toolbox.basics.bean.Unmarshaller;
import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.XmlMappingManager;

/**
 * Implementation of {@link XmlMappingManager}.
 */
public class BetwixtXmlMappingManagerImpl
    implements XmlMappingManager
{

  /**
   * {@link Marshaller} to use with betwixt.
   */
  private class BetwixtMarshaller
      implements Marshaller
  {

    /**
     * whether the xml produced is a fragment.
     */
    private final boolean xmlFragment;



    /**
     * constructor.
     * 
     * @param xmlFragment
     *          whether the xml produced is a fragment
     */
    public BetwixtMarshaller(boolean xmlFragment)
    {
      this.xmlFragment = xmlFragment;
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into a file.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param output
     *          File to be written. If this file already exists, it will be
     *          overwritten.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.File)
     */
    public void marshal(Object object, File output)
        throws TechnicalException
    {
      try {
        BeanWriter beanWriter = new BeanWriter(new FileOutputStream(output));
        initBeanWriter(beanWriter);
        if (!xmlFragment) {
          beanWriter.writeXmlDeclaration(XML_HEADING);
        }
        beanWriter.write(object);
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into an output
     * stream.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param os
     *          XML will be added to this stream.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.OutputStream)
     */
    public void marshal(Object object, OutputStream os)
        throws TechnicalException
    {
      try {
        BeanWriter beanWriter = new BeanWriter(os);
        initBeanWriter(beanWriter);
        if (!xmlFragment) {
          beanWriter.writeXmlDeclaration(XML_HEADING);
        }
        beanWriter.write(object);
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Marshal the content tree rooted at <tt>jaxbElement</tt> into a Writer.
     * 
     * @param object
     *          The root of content tree to be marshalled.
     * @param writer
     *          XML will be sent to this writer.
     * @throws TechnicalException
     *           If any unexpected problem occurs during the marshalling.
     * @see com.genia.toolbox.basics.bean.Marshaller#marshal(java.lang.Object,
     *      java.io.Writer)
     */
    public void marshal(Object object, Writer writer)
        throws TechnicalException
    {
      try {
        BeanWriter beanWriter = new BeanWriter(writer);
        initBeanWriter(beanWriter);
        if (!xmlFragment) {
          beanWriter.writeXmlDeclaration(XML_HEADING);
        }
        beanWriter.write(object);
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }

  }

  /**
   * {@link Unmarshaller} to use with betwixt.
   */
  private class BetwixtUnmarshaller
      implements Unmarshaller
  {

    /**
     * the current {@link BeanReader} to use.
     */
    private BeanReader currentBeanReader = null;

    /**
     * whether it is safe to release the {@link BeanReader} for further use.
     */
    private boolean releaseBeanReader = true;

    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object. A subclass
     * overrides the <code>finalize</code> method to dispose of system
     * resources or to perform other cleanup.
     * 
     * @throws Throwable
     *           the <code>Exception</code> raised by this method
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize()
        throws Throwable
    {
      try {
        if (currentBeanReader != null && releaseBeanReader) {
          release(currentBeanReader);
        }
      }
      finally {
        super.finalize();
      }
    }



    /**
     * returns the current {@link BeanReader} to use.
     * 
     * @return the current {@link BeanReader} to use
     * @throws TechnicalException
     *           if an error occured while trying to retrieve a
     *           {@link BeanReader}
     */
    private BeanReader getCurrentBeanReader()
        throws TechnicalException
    {
      if (currentBeanReader == null) {
        currentBeanReader = getBeanReader();
      }
      return currentBeanReader;
    }



    /**
     * Unmarshal XML data from the specified file and return the resulting
     * content tree.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param file
     *          the file to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.File)
     */
    public Object unmarshal(File file)
        throws TechnicalException
    {
      try {
        releaseBeanReader = false;
        Object result = getCurrentBeanReader().parse(file);
        releaseBeanReader = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Unmarshal XML data from the specified InputStream and return the
     * resulting content tree. Validation event location information may be
     * incomplete when using this form of the unmarshal API.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param is
     *          the InputStream to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.InputStream)
     */
    public Object unmarshal(InputStream is)
        throws TechnicalException
    {
      try {
        releaseBeanReader = false;
        Object result = getCurrentBeanReader().parse(is);
        releaseBeanReader = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }



    /**
     * Unmarshal XML data from the specified Reader and return the resulting
     * content tree. Validation event location information may be incomplete
     * when using this form of the unmarshal API, because a Reader does not
     * provide the system ID.
     * <p>
     * Implements <a href="#unmarshalGlobal">Unmarshal Global Root Element</a>.
     * 
     * @param reader
     *          the Reader to unmarshal XML data from
     * @return the newly created root object of the java content tree
     * @throws TechnicalException
     *           If any unexpected errors occur while unmarshalling
     * @see com.genia.toolbox.basics.bean.Unmarshaller#unmarshal(java.io.Reader)
     */
    public Object unmarshal(Reader reader)
        throws TechnicalException
    {
      try {
        releaseBeanReader = false;
        Object result = getCurrentBeanReader().parse(reader);
        releaseBeanReader = true;
        return result;
      }
      catch (Exception e) {
        throw getExceptionManager().convertException(e);
      }
    }

  }

  /**
   * default maximum number of {@link BeanReader} to keep.
   */
  private static final int DEFAULT_MAX_BEAN_READER = 5;

  /**
   * the xml heading to add when the produced xml is not a fragment.
   */
  private static final String XML_HEADING = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";

  /**
   * {@link Stack} of usable {@link BeanReader}.
   */
  private Stack<BeanReader> beanReaders = new Stack<BeanReader>();

  /**
   * the configuration files for the xml transformation.
   */
  private List<File> configurationFiles = new ArrayList<File>();

  /**
   * the resources containing the configurations for the xml transformation.
   */
  private List<String> configurationResources = new ArrayList<String>();

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * whether to use id in the mapping.
   */
  private boolean mapIds = false;

  /**
   * maximum number of {@link BeanReader} to keep.
   */
  private int maxBeanReader = DEFAULT_MAX_BEAN_READER;

  /**
   * the {@link ObjectStringConverter} to use.
   */
  private ObjectStringConverter objectStringConverter;

  /**
   * the {@link SimpleTypeMapper} to use.
   */
  private SimpleTypeMapper simpleTypeMapper;

  /**
   * the {@link TypeBindingStrategy} to use.
   */
  private TypeBindingStrategy typeBindingStrategy;

  /**
   * wheter to write empty elements.
   */
  private boolean writeEmptyElements = false;



  /**
   * returns the {@link BeanReader} to parse xml files.
   * 
   * @return the {@link BeanReader} to parse xml files
   * @throws TechnicalException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getBeanReader()
   */
  private BeanReader getBeanReader()
      throws TechnicalException
  {
    synchronized (this) {
      BeanReader beanReader = beanReaders.isEmpty() ? null : beanReaders.pop();
      if (beanReader == null) {
        beanReader = newBeanReader();
      }
      return beanReader;
    }
  }



  /**
   * getter for the configurationFiles property.
   * 
   * @return the configurationFiles
   */
  public List<File> getConfigurationFiles()
  {
    return configurationFiles;
  }



  /**
   * getter for the configurationResources property.
   * 
   * @return the configurationResources
   */
  public List<String> getConfigurationResources()
  {
    return configurationResources;
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * returns a {@link Marshaller} that allows to transform bean to xml.
   * 
   * @return a {@link Marshaller} that allows to transform bean to xml
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getMarshaller()
   */
  public Marshaller getMarshaller()
  {
    return getMarshaller(false);
  }



  /**
   * returns an {@link Unmarshaller} that allows to print a xml representation
   * of a bean.
   * 
   * @param xmlFragment
   *          whether the generated xml is a fragment
   * @return an {@link Unmarshaller} that allows to print a xml representation
   *         of a bean
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getMarshaller(boolean)
   */
  public Marshaller getMarshaller(boolean xmlFragment)
  {
    return new BetwixtMarshaller(xmlFragment);
  }



  /**
   * getter for the maxBeanReader property.
   * 
   * @return the maxBeanReader
   */
  public int getMaxBeanReader()
  {
    return maxBeanReader;
  }



  /**
   * getter for the objectStringConverter property.
   * 
   * @return the objectStringConverter
   */
  public ObjectStringConverter getObjectStringConverter()
  {
    return objectStringConverter;
  }



  /**
   * getter for the simpleTypeMapper property.
   * 
   * @return the simpleTypeMapper
   */
  public SimpleTypeMapper getSimpleTypeMapper()
  {
    return simpleTypeMapper;
  }



  /**
   * getter for the typeBindingStrategy property.
   * 
   * @return the typeBindingStrategy
   */
  public TypeBindingStrategy getTypeBindingStrategy()
  {
    return typeBindingStrategy;
  }



  /**
   * returns an {@link Unmarshaller} that allows to print a xml representation
   * of a bean.
   * 
   * @return an {@link Unmarshaller} that allows to print a xml representation
   *         of a bean
   * @see com.genia.toolbox.basics.manager.XmlMappingManager#getUnmarshaller()
   */
  public Unmarshaller getUnmarshaller()
  {
    return new BetwixtUnmarshaller();
  }



  /**
   * init the configuration of the given {@link BeanWriter}.
   * 
   * @param beanWriter
   *          the {@link BeanWriter} to init
   * @throws TechnicalException
   *           if an error occured
   */
  private void initBeanWriter(BeanWriter beanWriter)
      throws TechnicalException
  {
    BeanReader beanReader = getBeanReader();
    beanWriter.setBindingConfiguration(beanReader.getBindingConfiguration());
    beanWriter.setXMLIntrospector(beanReader.getXMLIntrospector());
    beanWriter.setWriteEmptyElements(isWriteEmptyElements());
    release(beanReader);
  }



  /**
   * getter for the mapIds property.
   * 
   * @return the mapIds
   */
  public boolean isMapIds()
  {
    return mapIds;
  }



  /**
   * getter for the writeEmptyElements property.
   * 
   * @return the writeEmptyElements
   */
  public boolean isWriteEmptyElements()
  {
    return writeEmptyElements;
  }



  /**
   * create a new {@link BeanReader} from the model.
   * 
   * @return a new {@link BeanReader}
   * @throws TechnicalException
   *           if an error occured
   */
  private BeanReader newBeanReader()
      throws TechnicalException
  {
    try {
      BeanReader beanReader = new BeanReader();
      beanReader.getBindingConfiguration().setMapIDs(isMapIds());
      if (getTypeBindingStrategy() != null) {
        beanReader.getXMLIntrospector().getConfiguration().setTypeBindingStrategy(getTypeBindingStrategy());
      }
      if (getObjectStringConverter() != null) {
        beanReader.getBindingConfiguration().setObjectStringConverter(getObjectStringConverter());
      }
      if (getSimpleTypeMapper() != null) {
        beanReader.getXMLIntrospector().getConfiguration().setSimpleTypeMapper(getSimpleTypeMapper());
      }
      // BeanCreationList chain = BeanCreationList.createStandardChain();
      // chain.insertBeanCreator(0, new CustomBeanCreator());
      // beanReader.getReadConfiguration().setBeanCreationChain(chain);

      // WARNING: This must be done last. Otherwise the configured mappers won't
      // be used.
      for (File file : getConfigurationFiles()) {
        beanReader.registerMultiMapping(new InputSource(new FileInputStream(file)));
      }
      for (String resource : getConfigurationResources()) {
        beanReader.registerMultiMapping(new InputSource(getClass().getResourceAsStream(resource)));
      }

      return beanReader;
    }
    catch (Exception e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * release an unused {@link BeanReader}.
   * 
   * @param beanReader
   *          the {@link BeanReader} to release
   */
  private void release(BeanReader beanReader)
  {
    if (beanReaders.size() < getMaxBeanReader()) {
      beanReaders.push(beanReader);
    }
  }



  /**
   * setter for the configurationFiles property.
   * 
   * @param configurationFiles
   *          the configurationFiles to set
   */
  public void setConfigurationFiles(List<File> configurationFiles)
  {
    this.configurationFiles = configurationFiles;
  }



  /**
   * setter for the configurationResources property.
   * 
   * @param configurationResources
   *          the configurationResources to set
   */
  public void setConfigurationResources(List<String> configurationResources)
  {
    this.configurationResources = configurationResources;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * setter for the mapIds property.
   * 
   * @param mapIds
   *          the mapIds to set
   */
  public void setMapIds(boolean mapIds)
  {
    this.mapIds = mapIds;
  }



  /**
   * setter for the maxBeanReader property.
   * 
   * @param maxBeanReader
   *          the maxBeanReader to set
   */
  public void setMaxBeanReader(int maxBeanReader)
  {
    this.maxBeanReader = maxBeanReader;
  }



  /**
   * setter for the objectStringConverter property.
   * 
   * @param objectStringConverter
   *          the objectStringConverter to set
   */
  public void setObjectStringConverter(ObjectStringConverter objectStringConverter)
  {
    this.objectStringConverter = objectStringConverter;
  }



  /**
   * setter for the simpleTypeMapper property.
   * 
   * @param simpleTypeMapper
   *          the simpleTypeMapper to set
   */
  public void setSimpleTypeMapper(SimpleTypeMapper simpleTypeMapper)
  {
    this.simpleTypeMapper = simpleTypeMapper;
  }



  /**
   * setter for the typeBindingStrategy property.
   * 
   * @param typeBindingStrategy
   *          the typeBindingStrategy to set
   */
  public void setTypeBindingStrategy(TypeBindingStrategy typeBindingStrategy)
  {
    this.typeBindingStrategy = typeBindingStrategy;
  }



  /**
   * setter for the writeEmptyElements property.
   * 
   * @param writeEmptyElements
   *          the writeEmptyElements to set
   */
  public void setWriteEmptyElements(boolean writeEmptyElements)
  {
    this.writeEmptyElements = writeEmptyElements;
  }

}
